/**
 * Copyright 2019 吉鼎科技.

 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.easyplatform.web.ext.jm;

import cn.easyplatform.web.ext.Queryable;
import cn.easyplatform.web.ext.Reloadable;
import cn.easyplatform.web.ext.Widget;
import cn.easyplatform.web.ext.ZkExt;
import cn.easyplatform.web.ext.jm.event.MoveEvent;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.DropEvent;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.ext.Readonly;
import org.zkoss.zul.impl.XulElement;


/**
 * @author <a href="mailto:davidchen@epclouds.com">littleDog</a> <br/>
 * @since 2.0.0 <br/>
 */
public class Mind extends XulElement implements Queryable, ZkExt, Reloadable, Readonly {

    /**
     * 新增结点事件
     */
    public final static String ON_NODE_ADD = "onAddNode";

    /**
     * 删除结点事件
     */
    public final static String ON_NODE_REMOVE = "onRemoveNode";

    /**
     * 更新结点事件
     */
    public final static String ON_NODE_UPDATE = "onUpdateNode";

    private final static ObjectMapper mapper = new ObjectMapper();

    static {
        mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
    }

    /**
     *
     */
    private static final long serialVersionUID = -8489741499316917861L;

    static {
        addClientEvent(Mind.class, Events.ON_SELECTION, CE_IMPORTANT
                | CE_REPEAT_IGNORE | CE_NON_DEFERRABLE);
        addClientEvent(Mind.class, Events.ON_OPEN, CE_IMPORTANT
                | CE_REPEAT_IGNORE | CE_NON_DEFERRABLE);
        addClientEvent(Mind.class, Events.ON_CLOSE, CE_IMPORTANT
                | CE_REPEAT_IGNORE | CE_NON_DEFERRABLE);
        addClientEvent(Mind.class, Events.ON_DOUBLE_CLICK, CE_IMPORTANT
                | CE_REPEAT_IGNORE | CE_NON_DEFERRABLE);
        addClientEvent(Mind.class, Events.ON_MOVE, CE_IMPORTANT
                | CE_REPEAT_IGNORE | CE_NON_DEFERRABLE);
        addClientEvent(Mind.class, ON_NODE_ADD, CE_IMPORTANT
                | CE_REPEAT_IGNORE | CE_NON_DEFERRABLE);
        addClientEvent(Mind.class, ON_NODE_REMOVE, CE_IMPORTANT
                | CE_REPEAT_IGNORE | CE_NON_DEFERRABLE);
        addClientEvent(Mind.class, ON_NODE_UPDATE, CE_IMPORTANT
                | CE_REPEAT_IGNORE | CE_NON_DEFERRABLE);
    }

    /**
     * 是否可编辑
     */
    private boolean _readonly;

    /**
     * 主题
     * <option value="">default</option>
     * <option value="primary">primary</option>
     * <option value="warning">warning</option>
     * <option value="danger">danger</option>
     * <option value="success">success</option>
     * <option value="info">info</option>
     * <option value="greensea" selected="selected">greensea</option>
     * <option value="nephrite">nephrite</option>
     * <option value="belizehole">belizehole</option>
     * <option value="wisteria">wisteria</option>
     * <option value="asphalt">asphalt</option>
     * <option value="orange">orange</option>
     * <option value="pumpkin">pumpkin</option>
     * <option value="pomegranate">pomegranate</option>
     * <option value="clouds">clouds</option>
     * <option value="asbestos">asbestos</option>
     */
    private String _theme;

    /**
     * 显示模式 full|side 默认full
     * 方向(full|left|right)
     */
    private String _orient;

    /**
     * 节点支持html元素
     */
    private boolean _html;

    /**
     * 绘制引擎 canvas|svg，默认canvas
     */
    private String _engine;

    /**
     * 思维导图距容器外框的最小水平距离（像素）
     */
    private int _hmargin;

    /**
     * 思维导图距容器外框的最小垂直距离（像素）
     */
    private int _vmargin;

    /**
     * 思维导图线条的粗细（像素）
     */
    private int _lineWidth;

    /**
     * 思维导图线条的颜色（html的颜色表示方法）
     */
    private String _lineColor;

    /**
     * 节点之间的水平间距（像素）
     */
    private int _hspace;

    /**
     * 节点之间的垂直间距（像素）
     */
    private int _vspace;

    /**
     * 节点收缩/展开控制器的尺寸（像素）
     */
    private int _pspace;

    /**
     * 是否启用快捷键
     */
    private boolean _shortcut;

    /**
     * 数据连接的资源id
     */
    private String _dbId;

    /**
     * 查询语句
     */
    private Object _query;

    /**
     * 过滤表达式
     */
    private String _filter;

    /**
     * 在显示时是否要马上执行查询
     */
    private boolean _immediate = true;

    /**
     * 选择的结点id
     */
    private Node _selectedNode;

    /**
     * 是否必需重新加载
     */
    private boolean _force;

    //以下为相关联的事件，原event对应到select事件
    private String openEvent;

    private String closeEvent;

    private String moveEvent;

    private String addEvent;

    private String removeEvent;

    private String updateEvent;

    public String getFilter() {
        return _filter;
    }

    public void setFilter(String filter) {
        this._filter = filter;
    }

    public boolean isShortcut() {
        return _shortcut;
    }

    public void setShortcut(boolean shortcut) {
        if (this._shortcut != shortcut) {
            this._shortcut = shortcut;
            smartUpdate("enable", shortcut);
        }
    }

    public String getTheme() {
        return _theme;
    }

    public void setTheme(String theme) {
        if (!Objects.equals(this._theme, theme)) {
            this._theme = theme;
            smartUpdate("theme", theme);
        }
    }

    public String getOrient() {
        return _orient;
    }

    public void setOrient(String orient) {
        if (!Objects.equals(this._orient, orient)) {
            this._orient = orient;
        }
    }

    public boolean isHtml() {
        return _html;
    }

    public void setHtml(boolean html) {
        if (this._html != html) {
            this._html = html;
            smartUpdate("html", html);
        }
    }

    public String getEngine() {
        return _engine;
    }

    public void setEngine(String engine) {
        if (!Objects.equals(this._engine, engine)) {
            this._engine = engine;
            smartUpdate("engine", engine);
        }
    }

    public int getHmargin() {
        return _hmargin;
    }

    public void setHmargin(int hmargin) {
        if (this._hmargin != hmargin) {
            this._hmargin = hmargin;
            smartUpdate("hmargin", hmargin);
        }
    }

    public int getVmargin() {
        return _vmargin;
    }

    public void setVmargin(int vmargin) {
        if (this._vmargin != vmargin) {
            this._vmargin = vmargin;
            smartUpdate("vmargin", vmargin);
        }
    }

    public int getLineWidth() {
        return _lineWidth;
    }

    public void setLineWidth(int lineWidth) {
        if (this._lineWidth != lineWidth) {
            this._lineWidth = lineWidth;
            smartUpdate("lineWidth", lineWidth);
        }
    }

    public String getLineColor() {
        return _lineColor;
    }

    public void setLineColor(String lineColor) {
        if (!Objects.equals(this._lineColor, lineColor)) {
            this._lineColor = lineColor;
            smartUpdate("lineColor", lineColor);
        }
    }

    public int getHspace() {
        return _hspace;
    }

    public void setHspace(int hspace) {
        if (this._hspace != hspace) {
            this._hspace = hspace;
            smartUpdate("hspace", hspace);
        }
    }

    public int getVspace() {
        return _vspace;
    }

    public void setVspace(int vspace) {
        if (this._vspace != vspace) {
            this._vspace = vspace;
            smartUpdate("vspace", vspace);
        }
    }

    public int getPspace() {
        return _pspace;
    }

    public void setPspace(int pspace) {
        if (this._pspace != pspace) {
            this._pspace = pspace;
            smartUpdate("pspace", pspace);
        }
    }

    @Override
    public boolean isForce() {
        return _force;
    }

    public void setForce(boolean force) {
        this._force = force;
    }

    @Override
    public boolean isReadonly() {
        return _readonly;
    }

    @Override
    public void setReadonly(boolean readonly) {
        if (this._readonly != readonly) {
            this._readonly = readonly;
            smartUpdate("editable", !readonly);
        }
    }

    public Object getQuery() {
        return _query;
    }

    public void setQuery(Object query) {
        this._query = query;
    }

    public String getDbId() {
        return _dbId;
    }

    public void setDbId(String dbId) {
        this._dbId = dbId;
    }

    public boolean isImmediate() {
        return _immediate;
    }

    public void setImmediate(boolean immediate) {
        this._immediate = immediate;
    }

    public boolean isChildable() {
        return false;
    }

    public String getOpenEvent() {
        return openEvent;
    }

    public void setOpenEvent(String openEvent) {
        this.openEvent = openEvent;
    }

    public String getCloseEvent() {
        return closeEvent;
    }

    public void setCloseEvent(String clsoeEvent) {
        this.closeEvent = clsoeEvent;
    }

    public String getMoveEvent() {
        return moveEvent;
    }

    public void setMoveEvent(String moveEvent) {
        this.moveEvent = moveEvent;
    }

    public String getAddEvent() {
        return addEvent;
    }

    public void setAddEvent(String addEvent) {
        this.addEvent = addEvent;
    }

    public String getRemoveEvent() {
        return removeEvent;
    }

    public void setRemoveEvent(String removeEvent) {
        this.removeEvent = removeEvent;
    }

    public String getUpdateEvent() {
        return updateEvent;
    }

    public void setUpdateEvent(String updateEvent) {
        this.updateEvent = updateEvent;
    }

    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer)
            throws java.io.IOException {
        super.renderProperties(renderer);
        render(renderer, "editable", !_readonly);
        render(renderer, "html", _html);
        render(renderer, "enable", _shortcut);
        if (!Strings.isBlank(_theme))
            render(renderer, "theme", _theme);
        if (!Strings.isBlank(_engine))
            render(renderer, "engine", _engine);
        if (!Strings.isBlank(_lineColor))
            render(renderer, "lineColor", _lineColor);
        if (_hmargin > 0)
            render(renderer, "hmargin", _hmargin);
        if (_vmargin > 0)
            render(renderer, "vmargin", _vmargin);
        if (_lineWidth > 0)
            render(renderer, "lineWidth", _lineWidth);
        if (_hspace > 0)
            render(renderer, "hspace", _hspace);
        if (_vspace > 0)
            render(renderer, "vspace", _vspace);
        if (_pspace > 0)
            render(renderer, "pspace", _pspace);

        if (rootNode != null) {
            if (rootNode.getChildren() != null) {//自动切分左右排
                if (this._orient == null || this._orient.equals("full")) {
                    int pos = rootNode.getChildren().size() / 2;
                    for (; pos < rootNode.getChildren().size(); pos++) {
                        Node node = rootNode.getChildren().get(pos);
                        if (node.getDirection() == null)
                            node.setDirection("left");
                    }
                } else {
                    for (Node n : rootNode.getChildren())
                        n.setDirection(this._orient);
                }
            }
            render(renderer, "model", mapper.writeValueAsString(rootNode));
        }
    }

    /**
     * 重绘
     */
    private void redraw() {
        if (rootNode != null) {
            if (rootNode.getChildren() != null) {//自动切分左右排
                if (this._orient == null || this._orient.equals("full")) {
                    int pos = rootNode.getChildren().size() / 2;
                    for (; pos < rootNode.getChildren().size(); pos++) {
                        Node node = rootNode.getChildren().get(pos);
                        if (node.getDirection() == null)
                            node.setDirection("left");
                    }
                } else {
                    for (Node n : rootNode.getChildren())
                        n.setDirection(this._orient);
                }
            }
            try {
                smartUpdate("model", mapper.writeValueAsString(rootNode));
            } catch (JsonProcessingException e) {
                throw new WrongValueException(this, e.getMessage());
            }
        }
    }

    public Node getSelectedNode() {
        return _selectedNode;
    }

    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        String cmd = request.getCommand();
        if (cmd.equals(Events.ON_SELECTION) || cmd.equals(Events.ON_OPEN) || cmd.equals(Events.ON_CLOSE) || cmd.equals(ON_NODE_REMOVE) || cmd.equals(Events.ON_DOUBLE_CLICK)) {
            Object id = request.getData().get("id");
            _selectedNode = findNode(id, rootNode);
            Events.postEvent(new Event(cmd, this, _selectedNode));
            if (cmd.equals(ON_NODE_REMOVE) && !_selectedNode.isIsroot()) {
                Node parent = findNode(_selectedNode.getParent().getId(), rootNode);
                parent.removeChild(_selectedNode);
                _selectedNode = null;
            }
        } else if (cmd.equals(Events.ON_DROP)) {
            Object id = request.getData().get("id");
            _selectedNode = findNode(id, rootNode);
            Events.postEvent(DropEvent.getDropEvent(request));
        } else if (cmd.equals(Events.ON_MOVE)) {
            Object id = request.getData().get("id");
            _selectedNode = findNode(id, rootNode);
            Object pid = request.getData().get("pid");
            Node op = findNode(_selectedNode.getParent().getId(), rootNode);
            Node np = findNode(pid, rootNode);
            //移除旧的父结点
            op.removeChild(_selectedNode);
            //设置新的父结点
            np.appendChild(_selectedNode);
            Events.postEvent(MoveEvent.getNodeEvent(request, _selectedNode));
        } else if (cmd.equals(ON_NODE_ADD)) {
            Object pid = request.getData().get("pid");
            Object id = request.getData().get("id");
            String topic = (String) request.getData().get("topic");
            Node parent = findNode(pid, rootNode);
            _selectedNode = parent.appendChild(id, topic);
            Events.postEvent(new Event(cmd, this, _selectedNode));
        } else if (cmd.equals(ON_NODE_UPDATE)) {
            Object id = request.getData().get("id");
            String topic = (String) request.getData().get("topic");
            _selectedNode = findNode(id, rootNode);
            _selectedNode.setTopic(topic);
            Events.postEvent(new Event(cmd, this, _selectedNode));
        } else
            super.service(request, everError);
    }

    private Node findNode(Object id, Node node) {
        if (Objects.equals(id.toString(), node.getId().toString()))
            return node;
        if (node.getChildren() != null) {
            for (Node n : node.getChildren()) {
                if (Objects.equals(n.getId().toString(), id.toString()))
                    return n;
                Node rn = findNode(id, n);
                if (rn != null)
                    return rn;
            }
        }
        return null;
    }

    /**
     * 在选中的结点添加子结点，返回父结点
     *
     * @param child
     * @return
     */
    public Node appendNode(Node child) {
        if (_selectedNode == null)
            _selectedNode = rootNode;
        _selectedNode.appendChild(child);
        redraw();
        return _selectedNode;
    }

    /**
     * 添加子结点，返回父结点
     *
     * @param child
     * @return
     */
    public Node appendNode(Object parent, Node child) {
        Object pid = parent instanceof Node ? ((Node) parent).getId() : parent;
        Node pn = findNode(pid, rootNode);
        if (pn != null) {
            pn.appendChild(child);
            redraw();
            return pn;
        }
        return null;
    }

    /**
     * 移除指定id的结点，返回父结点
     *
     * @param n
     * @return
     */
    public Node removeNode(Object n) {
        Object id = n instanceof Node ? ((Node) n).getId() : n;
        Node node = findNode(id, rootNode);
        if (node != null) {
            Node parent = findNode(node.getParent().getId(), rootNode);
            if (parent != null)
                parent.removeChild(node);
            redraw();
            return parent;
        }
        return null;
    }

    @Override
    public void reload() {
        Widget ext = (Widget) getAttribute("$proxy");
        if (ext != null)
            ext.reload(this);
        redraw();
    }

    public Node root(Object id, String topic) {
        rootNode = new Node(id, topic);
        rootNode.setIsroot(true);
        return rootNode;
    }

    public Node root() {
        return rootNode;
    }

    private Node rootNode;
}
